import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.swing.JProgressBar;

import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;

public class GeometricRecognizer
{

    public static final int NUM_RESAMPLE_POINTS = 64;
    private static final double DX = 250.0;
    public static final SizeR RESAMPLE_SCALE = new SizeR(DX, DX);
    public static final double DIAGONAL = Math.sqrt(DX * DX + DX * DX);
    public static final double HALF_DIAGONAL = 0.5 * DIAGONAL;
    public static final PointR RESAMPLE_ORIGIN = new PointR(0, 0);
    // Golden Ratio
    private static final double PHI = 0.5 * (-1 + Math.sqrt(5));

    // dc
    public static final double TOLERANCE = 0.75;

    // batch testing
    private static final int NUM_RANDOM_TESTS = 100;

    // TODO: Probably not the good way to implement this. This is actually an
    // event sent to the UI with the progress of the tests accomplished so far
    // in testBatch function.
    // It will, I think, will be used in a progress bar. To be continued...
    protected JProgressBar barTests = new JProgressBar(0, 100);

    private Map<String, Gesture> gestures;

    public GeometricRecognizer()
    {
        gestures = new Hashtable<String, Gesture>(256);
    }

    // Candidate points
    public NBestList recognize(ArrayList<PointR> pointsparam)
    {

        // Copy the parameter
        ArrayList<PointR> points = new ArrayList<PointR>(pointsparam);

        // resample to a common number of points
        points = Utils.resample(points, NUM_RESAMPLE_POINTS);

        // rotate so that the centroid-to-1st-point is at zero degrees
        // Indicative angle
        double radians = Utils.angleInRadians(Utils.centroid(points), (PointR) points.get(0),
                false);
        // Undo angle
        points = Utils.rotateByRadians(points, -1.0 * radians);

        // Scale to a common (square) dimension
        points = Utils.scaleTo(points, RESAMPLE_SCALE);

        // translate to a common origin
        points = Utils.translateCentroidTo(points, RESAMPLE_ORIGIN);

        NBestList nbest = new NBestList();
        Gesture p = null;

        for (Map.Entry<String, Gesture> entry : gestures.entrySet())
        {
            p = entry.getValue();
            double[] best = goldenSectionSearch(
            // to rotate
                    points,
                    // to match
                    p.getPoints(),
                    // lbound
                    Utils.degToRad(-45.0),
                    // ubound
                    Utils.degToRad(+45.0),
                    // threshold
                    Utils.degToRad(2.0));
            double score = 1d - best[0] / HALF_DIAGONAL;
            // name, score, distance, angle
            nbest.addResult(p.getName(), score, best[0], best[1]);
        }
        nbest.addResult("Try Again", TOLERANCE, 0.0, 0.0);
        // Sort so that nbest[0] is best result
        nbest.sortDescending();
        return nbest;
    }

    private double[] goldenSectionSearch(ArrayList<PointR> pts1, ArrayList<PointR> pts2,
            double aParam, double bParam, double threshold)
    {

        double a = aParam;
        double b = bParam;

        double x1 = PHI * a + (1 - PHI) * b;
        ArrayList<PointR> newPoints = Utils.rotateByRadians(pts1, x1);
        double fx1 = Utils.pathDistance(newPoints, pts2);

        double x2 = (1 - PHI) * a + PHI * b;
        newPoints = Utils.rotateByRadians(pts1, x2);
        double fx2 = Utils.pathDistance(newPoints, pts2);

        // Calls
        double i = 2.0;
        while (Math.abs(b - a) > threshold)
        {
            if (fx1 < fx2)
            {
                b = x2;
                x2 = x1;
                fx2 = fx1;
                x1 = PHI * a + (1 - PHI) * b;
                newPoints = Utils.rotateByRadians(pts1, x1);
                fx1 = Utils.pathDistance(newPoints, pts2);
            }
            else
            {
                a = x1;
                x1 = x2;
                fx1 = fx2;
                x2 = (1 - PHI) * a + PHI * b;
                newPoints = Utils.rotateByRadians(pts1, x2);
                fx2 = Utils.pathDistance(newPoints, pts2);
            }
            i++;
        }
        // distance, angle, calls to pathdist
        double x[] = { Math.min(fx1, fx2), Utils.radToDeg((b + a) / 2.0), i };
        return x;
    }

    // continues to rotate 'pts1' by 'step' degrees as long as points become
    // ever-closer
    // in path-distance to pts2. the initial distance is given by 'distance'.
    // the best distance
    // is returned in array[0], while the angle at which it was achieved is in
    // array[1].
    // array[3] contains the number of calls to PathDistance.

    private double[] hillClimbSearch(ArrayList<PointR> pts1, ArrayList<PointR> pts2,
            double distance, double step)
    {
        double distanceCopy = distance;
        double i = 0.0;
        double theta = 0.0;
        double d = distanceCopy;
        do
        {
            // the last angle tried was better still
            distanceCopy = d;
            theta += step;
            ArrayList<PointR> newPoints = Utils.rotateByDegrees(pts1, theta);
            d = Utils.pathDistance(newPoints, pts2);
            i++;
        } while (d <= distanceCopy);
        // distance, angle, calls to pathdist
        double x[] = { distanceCopy, theta - step, i };
        return x;
    }

    private double[] fullSearch(ArrayList<PointR> pts1, ArrayList<PointR> pts2,
            BufferedWriter writer)
    {
        double bestA = 0d;
        double bestD = Utils.pathDistance(pts1, pts2);

        for (int i = -180; i <= +180; i++)
        {
            ArrayList<PointR> newPoints = Utils.rotateByDegrees(pts1, i);
            double d = Utils.pathDistance(newPoints, pts2);
            if (writer != null)
            {
                try
                {
                    writer.write(i + "\t" + new DecimalFormat("##.##").format(Math.round(d)));
                } catch (IOException e)
                {
                }
            }
            if (d < bestD)
            {
                bestD = d;
                bestA = i;
            }
        }
        try
        {
            writer.write("\nFull Search (360 rotations)\n"
                    + new DecimalFormat("##.##").format(Math.round(bestA)) + (char) 176 + "\t"
                    + new DecimalFormat("##.###").format(Math.round(bestD)));
        } catch (IOException e)
        {
        }
        // distance, angle, calls to pathdist
        double x[] = { bestD, bestA, 360.0 };
        return x;
    }

    public int getNumGestures()
    {
        return gestures.size();
    }

    public void clearGestures()
    {
        gestures.clear();
    }

    public List<Gesture> getGestures()
    {
        List<Gesture> list = new ArrayList<Gesture>(gestures.values());
        Collections.sort(list);
        return list;
    }

    public boolean saveGesture(String filename, ArrayList<PointR> points)
    {

        if (filename == null || points == null)
        {
            return false;
        }
        // add the new prototype with the name extracted from the filename.
        String name = Gesture.parseName(filename);
        if (gestures.containsKey(name))
        {
            gestures.remove(name);
        }
        Gesture newPrototype = new Gesture(name, points);
        gestures.put(name, newPrototype);

        // figure out the duration of the gesture
        PointR p0 = (PointR) points.get(0);
        PointR pn = (PointR) points.get(points.size() - 1);

        // do the xml writing
        boolean success = true;

        try
        {
            // save the prototype as an Xml file
            Element gesture = new Element("Gesture");
            ArrayList<Attribute> listAttributes = new ArrayList<Attribute>();
            listAttributes.add(new Attribute("Name", name));
            listAttributes.add(new Attribute("NumPts", String.valueOf(points.size())));
            listAttributes.add(new Attribute("Millseconds", String.valueOf(pn.t - p0.t)));
            // TODO: Use the implementationTitle
            listAttributes.add(new Attribute("AppName", "WUW01"));
            // TODO: Use the implementationVersion
            listAttributes.add(new Attribute("AppVer", "1.0.0.0"));
            listAttributes.add(new Attribute("Date", new Date().toString()));
            listAttributes.add(new Attribute("TimeOfDay", new Date().toString()));
            gesture.setAttributes(listAttributes);

            Document doc = new Document(gesture);

            // write out the raw individual points
            for (PointR p : points)
            {
                Element point = new Element("Point");
                point.setAttribute(new Attribute("X", String.valueOf((int) p.x)));
                point.setAttribute(new Attribute("Y", String.valueOf((int) p.y)));
                point.setAttribute(new Attribute("T", String.valueOf((int) p.t)));
                doc.getRootElement().addContent(point);
            }
            XMLOutputter xmlOutput = new XMLOutputter();
            xmlOutput.setFormat(Format.getPrettyFormat());

            // TODO: We will have to change how to save the file. Because when
            // we will have a executable JAR file, it will be impossible to
            // dynamically add new files into it (well I'm pretty sure about
            // that,
            // but maybe I'm wrong ?)
            // One way to bypass this limitation : When we're executing the app
            // with
            // the JAR file :
            // 1. Create a temporary directory
            // 2. Add the new files (like new gesture files)
            // 3. Add into the exiting application listener, either :
            // 4.1. Recreate a jar file with the new files, and delete the old
            // one
            // (and the temp directory).
            // 4.2. Execute the jar command with the 'u' option, which you can
            // use
            // to update the contents of an existing JAR file.
            xmlOutput.output(doc, new FileWriter(filename));

        } catch (IOException e1)
        {
            success = false;
        }

        // Xml file successfully written (or not)
        return success;
    }

    public boolean loadGesture(String filename)
    {
        boolean success = true;

        SAXBuilder sxb = new SAXBuilder();
        Document document;
        Gesture p = null;
        try
        {
            document = sxb.build(getClass().getResourceAsStream(filename));
            p = readGesture(document);
        } catch (JDOMException | IOException e1)
        {
            success = false;
        }
        if (success)
        {
            // remove any with the same name and add the prototype gesture
            if (gestures.size() > 0)
            {
                if (gestures.containsKey(p.getName()))
                {
                    gestures.remove(p.getName());
                }
            }
            gestures.put(p.getName(), p);
        }
        return success;
    }

    // assumes the reader has been just move to the head of the content.
    private Gesture readGesture(Document document)
    {

        Element racine = document.getRootElement();
        assert (racine.getName().equals("Gesture"));

        String name = racine.getAttributeValue("Name");
        ArrayList<PointR> points = new ArrayList<PointR>(Integer.parseInt(racine
                .getAttributeValue("NumPts")));
        List<Element> xmlPts = racine.getChildren();

        Iterator<Element> i = xmlPts.iterator();
        while (i.hasNext())
        {
            Element courant = i.next();
            int x = Integer.parseInt(courant.getAttributeValue("X"));
            int y = Integer.parseInt(courant.getAttributeValue("Y"));
            int t = Integer.parseInt(courant.getAttributeValue("T"));
            PointR point = new PointR(x, y, t);
            points.add(point);
        }
            return new Gesture(name, points);
    }

    /**
     * <summary> Assemble the gesture filenames into categories that contain
     * potentially multiple examples of the same gesture. </summary> <param
     * name="filenames"></param> <returns>A 1D list of category instances that
     * each contain the same number of examples, or <b>null</b> if an error
     * occurs. </returns> <remarks> See the comments above
     * MainForm.BatchProcess_Click. </remarks>
     **/
    public List<Category> assembleBatch(String filenames[])
    {

        Map<String, Category> categories = new Hashtable<String, Category>();
        SAXBuilder sxb = new SAXBuilder();
        Document document;

        for (int i = 0; i < filenames.length; i++)
        {
            String filename = filenames[i];

            try
            {
                document = sxb.build(getClass().getResourceAsStream(filename));

                Gesture p = readGesture(document);
                String catName = Category.parseName(p.getName());
                if (categories.containsKey(catName))
                {
                    Category cat = (Category) categories.get(catName);
                    // if the category has been made before, just add to it.
                    try
                    {
                        cat.addExample(p);
                    } catch (Exception e)
                    {
                    }
                }
                else
                {
                    // create new category
                    Category tmp = null;
                    try
                    {
                        tmp = new Category(catName, p);
                    } catch (Exception e)
                    {
                    }
                    categories.put(catName, tmp);
                }

            } catch (JDOMException | IOException e)
            {
                categories.clear();
                categories = null;
            }
        }

        // now make sure that each category has the same number of elements in
        // it
        List<Category> list = null;
        if (categories != null)
        {
            list = new ArrayList<Category>(categories.values());
            int numExamples = ((Category) list.get(0)).numExamples();
            for (Category c : list)
            {
                if (c.numExamples() != numExamples)
                {
                    list.clear();
                    list = null;
                    System.err.println("Different number of examples in gesture categories.");
                    break;
                }
            }
        }
        return list;
    }

    /**
     * <summary> Tests an entire batch of files. See comments atop
     * MainForm.TestBatch_Click(). </summary> <param name="subject">Subject
     * number.</param> <param name="speed">"fast", "medium", or "slow"</param>
     * <param name="categories">A list of gesture categories that each contain
     * lists of prototypes (examples) within that gesture category.</param>
     * <param name="dir">The directory into which to write the output
     * files.</param> <returns>True if successful; false otherwise. </returns>
     **/
    public boolean testBatch(int subject, String speed, List<Category> categories, String dir)
    {

        // Set up a main results file and detailed recognition results file
        boolean success = true;
        BufferedWriter mainWriter = null;
        BufferedWriter recWriter = null;

        // TODO: Not the best way to replace the Environment.TickCount C# stuff.
        int start = (int) (System.currentTimeMillis());

        String mainFile = dir + "geometric_main_" + start + ".txt";
        String recFile = dir + "geometric_data_" + start + ".txt";

        try
        {
            mainWriter = Files.newBufferedWriter(Paths.get(mainFile), StandardCharsets.UTF_8);

            mainWriter.write("Subject = " + subject + ", Recognizer = geometric, Speed = " + speed
                    + ", StartTime(ms) = " + start);
            mainWriter.write("Subject Recognizer Speed NumTraining GestureType RecognitionRate\n");

            recWriter = Files.newBufferedWriter(Paths.get(recFile), StandardCharsets.UTF_8);
            recWriter.write("Subject = " + subject + ", Recognizer = geometric, Speed = " + speed
                    + ", StartTime(ms) = " + start);
            recWriter
                    .write("Correct? NumTrain Tested 1stCorrect Pts Ms Angle : (NBestNames) [NBestScores]\n");
        } catch (IOException e)
        {
            if (mainWriter != null)
            {
                try
                {
                    mainWriter.close();
                } catch (IOException e1)
                {
                }
            }
            if (recWriter != null)
            {
                try
                {
                    recWriter.close();
                } catch (IOException e1)
                {
                }
            }
        }

        // Determine the number of gesture categories and the number of examples
        // in each one.
        int numCategories = categories.size();
        int numExamples = ((Category) categories.get(0)).numExamples();
        double totalTests = (numExamples - 1) * NUM_RANDOM_TESTS;

        // Outermost loop: trains on N=1..9, tests on 10-N (for e.g.,
        // numExamples = 10)
        for (int n = 1; n <= numExamples - 1; n++)
        {
            // Storage for the final avg results for each category for this N
            double results[] = new double[numCategories];

            // Run a number of tests at this particular N number of training
            // examples
            for (int r = 0; r < NUM_RANDOM_TESTS; r++)
            {

                // Clear any (old) loaded prototypes
                gestures.clear();

                // Load (train on) N randomly selected gestures in each category
                for (int i = 0; i < numCategories; i++)
                {

                    // The category to load N examples for
                    Category c = (Category) categories.get(i);
                    // Select N unique indices
                    List<Integer> tmplist = new ArrayList<Integer>();
                    for (int a = 0; a < n; a++)
                    {
                        tmplist.add((int) (Math.random() * numExamples - 1));
                    }
                    Collections.shuffle(tmplist);

                    for (int j = 0; j < tmplist.size(); j++)
                    {
                        // Get the prototype from this category at chosen[j]
                        Gesture p = null;
                        try
                        {
                            p = c.getGesture(tmplist.get(j));
                        } catch (Exception e)
                        {
                        }
                        // Load the randomly selected test gesture into the
                        // recognizer
                        gestures.put(p.getName(), p);
                    }
                }
                // Testing loop on all unloaded gestures in each category.
                // Creates a recognition
                // rate (%) by averaging the binary outcomes (correct,
                // incorrect) for each test.

                for (int i = 0; i < numCategories; i++)
                {

                    // pick a random unloaded gesture in this category for
                    // testing
                    // instead of dumbly picking, first find out what indices
                    // aren't
                    // loaded, and then randomly pick from those.
                    Category c = (Category) categories.get(i);
                    int[] notLoaded = new int[numExamples - n];
                    for (int j = 0, k = 0; j < numExamples; j++)
                    {
                        Gesture g = null;
                        try
                        {
                            g = c.getGesture(j);
                        } catch (Exception e)
                        {
                        }
                        if (!gestures.containsKey(g.getName()))
                        {
                            // jth gesture in c is not loaded
                            notLoaded[k++] = j;
                        }
                    }
                    // index
                    int chosen = (int) (Math.random() * notLoaded.length - 1);
                    // gesture to test
                    Gesture p = null;
                    try
                    {
                        p = c.getGesture(notLoaded[chosen]);
                    } catch (Exception e)
                    {
                    }

                    // Do the recognition !
                    ArrayList<PointR> testPts = Utils.rotateByDegrees(p.getRawPoints(),
                            Math.random() * 360);
                    NBestList result = recognize(testPts);
                    String category = Category.parseName(result.name());
                    int correct = (c.getName().equals(category)) ? 1 : 0;

                    try
                    {
                        recWriter.write(correct + " " + n + " " + p.getName() + " "
                                + firstCorrect(p.getName(), result.names()) + " "
                                + p.getRawPoints().size() + " " + p.duration() + " "
                                + new DecimalFormat("##.#").format(Math.round(result.angle()))
                                + (char) 176 + " : (" + result.namesString() + ") ["
                                + result.scoresString() + "]");
                    } catch (IOException e)
                    {
                    }

                    results[i] += correct;
                }

                // provide feedback as to how many tests have been performed
                // thus far.
                double testsSoFar = ((n - 1) * NUM_RANDOM_TESTS) + r;
                // callback
                // TODO: See barTests TODO.
                barTests.setValue((int) (testsSoFar / totalTests));
                barTests.repaint();
            }

            //
            // now create the final results for this N and write them to a file
            //
            for (int i = 0; i < numCategories; i++)
            {
                // normalize by the number of tests at this N
                results[i] /= (double) NUM_RANDOM_TESTS;
                Category c = (Category) categories.get(i);
                // Subject Recognizer Speed NumTraining GestureType
                // RecognitionRate
                try
                {
                    mainWriter.write(subject + " geometric " + speed + " " + n + " " + c.getName()
                            + " " + new DecimalFormat("##.###").format(Math.round(results[i])));
                } catch (IOException e)
                {
                }
            }
        }

        // time-stamp the end of the processing
        // TODO: More accurate tickcount needed.
        int end = (int) System.currentTimeMillis();
        try
        {
            mainWriter.write("\nEndTime(ms) = " + end + ", Minutes = "
                    + new DecimalFormat("##.##").format(Math.round(end - start / 60000.0)));
            recWriter.write("\nEndTime(ms) = " + end + ", Minutes = "
                    + new DecimalFormat("##.##").format(Math.round(end - start / 60000.0)));
        } catch (IOException e1)
        {
        }

        if (mainWriter != null)
        {
            try
            {
                mainWriter.close();
                recWriter.close();
            } catch (Exception e)
            {
            }
        }
        return success;
    }

    private int firstCorrect(String name, ArrayList<String> names)
    {
        String category = Category.parseName(name);
        for (int i = 0; i < names.size(); i++)
        {
            String c = Category.parseName(names.get(i));
            if (category.equals(c))
            {
                return i + 1;
            }
        }
        return -1;
    }

    public boolean createRotationGraph(String file1, String file2, String dir, boolean similar)
    {
        boolean success = true;

        SAXBuilder sxb = new SAXBuilder();
        Document document1 = null;
        Document document2 = null;
        try
        {
            document1 = sxb.build(getClass().getResourceAsStream(file1));
            document2 = sxb.build(getClass().getResourceAsStream(file2));
        } catch (JDOMException | IOException e1)
        {
        }

        BufferedWriter writer = null;
        try
        {
            // read gesture file #1
            Gesture g1 = readGesture(document1);

            // read gesture file #2
            Gesture g2 = readGesture(document2);

            // create output file for results
            String os = System.getProperty("os.name").toLowerCase();
            char separator = 0;
            if (os.indexOf("win") >= 0)
            {
                separator = '\\';
            }
            else
            {
                separator = '/';
            }
            String outfile = dir + separator + (similar ? "o" : "x") + "(" + g1.getName() + ", "
                    + g2.getName() + ")_" + (int) System.currentTimeMillis() + ".txt";
            writer = Files.newBufferedWriter(Paths.get(outfile), StandardCharsets.UTF_8);
            writer.write("Rotated: " + g1.getName() + " --> " + g2.getName() + ". "
                    + new Date().toString() + ", " + "\n");

            // do the full 360 degree rotations
            double[] full = fullSearch(g1.getPoints(), g2.getPoints(), writer);

            // use bidirectional hill climbing to do it again
            // initial distance
            double init = Utils.pathDistance(g1.getPoints(), g2.getPoints());
            double[] pos = hillClimbSearch(g1.getPoints(), g2.getPoints(), init, 1d);
            double[] neg = hillClimbSearch(g1.getPoints(), g2.getPoints(), init, -1d);
            double[] best = new double[3];
            // min distance
            best = (neg[0] < pos[0]) ? neg : pos;
            double tmp = pos[2] + neg[2] + 1;
            writer.write("\nHill Climb Search (" + tmp + " rotations)\n"
                    + new DecimalFormat("##.##").format(Math.round(best[1])) + (char) 176 + "\t"
                    + new DecimalFormat("##.###").format(Math.round(best[0])) + "px");

            // use golden section search to do it yet again
            // 1. to rotate
            // 2. to match
            // 3. lbound
            // 4. ubound
            // 5. threshold
            double[] gold = goldenSectionSearch(g1.getPoints(), g2.getPoints(),
                    Utils.degToRad(-45.0), Utils.degToRad(+45.0), Utils.degToRad(2.0));

            // calls, angle, distance
            writer.write("\nGolden Section Search (" + gold[2] + " rotations)\n"
                    + new DecimalFormat("##.##").format(Math.round(gold[1])) + (char) 176 + "\t"
                    + new DecimalFormat("##.###").format(Math.round(gold[0])) + "px");

            // for pasting into Excel
            // 1. rotated
            // 2. into
            // 3. |angle|
            // 4. Full Search angle
            // 5. Full Search distance
            // 6. Initial distance w/o any search
            // 7. Full Search iterations
            // 8. |angle|
            // 9. Bidirectional Hill Climb Search angle
            // 10. Bidirectional Hill Climb Search distance
            // 11. Bidirectional Hill Climb Search iterations
            // 12. |angle|
            // 13. Golden Section Search angle
            // 14. Golden Section Search distance
            // 15. Golden Section Search iterations
            writer.write("\n" + g1.getName() + " " + g2.getName() + " "
                    + new DecimalFormat("##.##").format(Math.abs(Math.round(full[1]))) + " "
                    + new DecimalFormat("##.##").format(Math.round(full[1])) + " "
                    + new DecimalFormat("##.###").format(Math.round(full[0])) + " "
                    + new DecimalFormat("##.###").format(Math.round(init)) + " " + full[2] + " "
                    + new DecimalFormat("##.##").format(Math.abs(Math.round(best[1]))) + " "
                    + new DecimalFormat("##.##").format(Math.round(best[1])) + " "
                    + new DecimalFormat("##.###").format(Math.round(best[0])) + " "
                    + (pos[2] + neg[2] + 1) + " "
                    + new DecimalFormat("##.##").format(Math.abs(Math.round(gold[1]))) + " "
                    + new DecimalFormat("##.##").format(Math.round(gold[1])) + " "
                    + new DecimalFormat("##.###").format(Math.round(gold[0])) + " " + gold[2]);
        } catch (Exception e)
        {
            success = false;
        } finally
        {
            if (writer != null)
            {
                try
                {
                    writer.close();
                } catch (IOException e)
                {
                }
            }
        }
        return success;
    }
}